
/*!
    \copyright  Copyright (c) 2024 Qualcomm Technologies International, Ltd.
                All Rights Reserved.
                Qualcomm Technologies International, Ltd. Confidential and Proprietary.
    \file
    @{
        \ingroup    adkfs
        \brief      AdkFs In-Memory Filesystem provider

*/
#include "adkfs_mem_logging.h"
#include "adkfs_provider_if.h"

#include <data_blob_types.h>

#include <panic.h>
#include <file/file.h>

#include <stdint.h>
#include <stddef.h>
#include <stdbool.h>
#include <stdlib.h>
#include <errno.h>
#include <string.h>

#define ADKFS_MEM_MAX_PATHLEN               ((size_t)(64))
#define ADKFS_MEM_MAX_OPEN_FILES            8
#define ADKFS_MEM_MAX_HANDLES_PER_FILE      2

#define ADKFS_MEM_INVALID_FILE_IDX          ADKFS_MEM_MAX_OPEN_FILES
#define ADKFS_MEM_INVALID_HANDLE_IDX        ADKFS_MEM_MAX_HANDLES_PER_FILE

typedef size_t mem_file_idx_t;

typedef struct {
    /*! The file index in the file manager */
    mem_file_idx_t idx;
    /*! The access mode */
    adkfs_access_flags_t access;
    /*! The file position pointer for read/write operations */
    size_t offset;
} mem_file_handle_t;

typedef struct {
    /*! The "File" itself */
    data_blob_t blob;
    /*! The "File" path */
    const char* path;
    /*! Array of active handles on the file */
    mem_file_handle_t handles[ADKFS_MEM_MAX_HANDLES_PER_FILE];
} mem_file_t;

#ifdef HOSTED_TEST_ENVIRONMENT
#define STATIC
#else
#define STATIC  static
#endif /* HOSTED_TEST_ENVIRONMENT */

STATIC mem_file_t* adkfs_mem_file_mgr[ADKFS_MEM_MAX_OPEN_FILES];

/*!
 * \brief Find a file by path
 *
 * \param[in]   path: Path to find
 * \param[out]  idx: Index of the found file.
 *              If file is not found it returns the first free slot or
 *              ADKFS_MEM_INVALID_FILE_IDX if no free slots are available
 * 
 * \return TRUE if path is found, FALSE otherwise
 */
static bool find_file_by_path(const char *path, mem_file_idx_t *idx)
{
    mem_file_idx_t free_idx = ADKFS_MEM_INVALID_FILE_IDX;

    for (size_t i = 0; i < ADKFS_MEM_MAX_OPEN_FILES; i++)
    {
        mem_file_t *file = adkfs_mem_file_mgr[i];
        if (file)
        {
            if (0 == strncmp(file->path, path, ADKFS_MEM_MAX_PATHLEN))
            {
                *idx = i;
                return TRUE;
            }
        }
        else if (ADKFS_MEM_INVALID_FILE_IDX == free_idx)
        {
            free_idx = i;
        }
    }
    *idx = free_idx;
    return FALSE;
}

/*!
 * \brief Gets the initial file position when opening a file
 * 
 * \param[in] access: the access mode to open the file
 * 
 * \return TRUE if file position is at the end of the file, FALSE if file position is at the beginning of the file
*/
static bool starting_file_position_is_eof(adkfs_access_flags_t access)
{
    switch(access)
    {
        case ADKFS_FOPEN_WRITE:
        /*lint -fallthrough */
        case ADKFS_FOPEN_READ_WRITE:
        /*lint -fallthrough */
        case ADKFS_FOPEN_READ:
        /*lint -fallthrough */
        case ADKFS_FOPEN_READ_WRITE_EXISTING:
        {
            return FALSE;
        }
        break;

        case ADKFS_FOPEN_WRITE_APPEND:
        /*lint -fallthrough */
        case ADKFS_FOPEN_APPEND:
        {
            return TRUE;
        }
        break;
    }
}

static void reset_handle(mem_file_handle_t *handle)
{
    handle->idx = ADKFS_MEM_INVALID_HANDLE_IDX;
    handle->access = 0;
    handle->offset = 0;
}

static int32_t AdkFsMem_Fopen(mem_file_handle_t **handle, const char *path, adkfs_access_flags_t access)
{
    DEBUG_LOG("AdkFsMem_Fopen: path=%s access=enum:adkfs_access_flags_t:%u", path, access);
    PanicFalse(strlen(path) <= ADKFS_MEM_MAX_PATHLEN);

    int32_t result = 0;
    *handle = NULL;

    mem_file_idx_t idx = ADKFS_MEM_INVALID_FILE_IDX;
    if (find_file_by_path(path, &idx))
    {
        mem_file_t *file = adkfs_mem_file_mgr[idx];

        for (size_t i = 0; i < ADKFS_MEM_MAX_HANDLES_PER_FILE; i++)
        {
            if (ADKFS_MEM_INVALID_HANDLE_IDX == file->handles[i].idx)
            {
                file->handles[i].idx = idx;
                file->handles[i].access = access;

                if (ADKFS_FOPEN_WRITE == access || ADKFS_FOPEN_READ_WRITE == access)
                {
                    if (file->blob.data)
                    {
                        DataBlob_Free(&file->blob);
                    }
                }

                if (starting_file_position_is_eof(access))
                {
                    file->handles[i].offset = file->blob.data_length;
                }
                else
                {
                    file->handles[i].offset = 0;
                }

                *handle = &file->handles[i];
                break;
            }
        }

        if (!*handle)
        {
            result = ENFILE;
        }
    }
    else /* File doesn't exist */
    {
        if ((ADKFS_FOPEN_READ == access) || (ADKFS_FOPEN_READ_WRITE_EXISTING == access) || (ADKFS_FOPEN_APPEND == access))
        {
            /* These state that the file MUST exist so return an error */
            result = ENOENT;
        }
        else
        {
            if (ADKFS_MEM_INVALID_FILE_IDX != idx)
            {
                /* Create a new file */
                mem_file_t *file = PanicUnlessNew(mem_file_t);
                memset(file, 0, sizeof(*file));

                size_t pathlen = strlen(path);
                file->path = calloc(pathlen + 1, sizeof(char));
                strncpy(file->path, path, pathlen);

                for (size_t n = 0; n < ADKFS_MEM_MAX_HANDLES_PER_FILE; n++)
                {
                    reset_handle(&file->handles[n]);
                }

                file->handles[0].idx = idx;
                file->handles[0].access = access;
                file->handles[0].offset = 0;

                adkfs_mem_file_mgr[idx] = file;

                *handle = &file->handles[0];
            }
            else
            {
                /* No more free slots. Maximum number of open files */
                result = ENFILE;
            }
        }
    }
    return result;
}

static int32_t AdkFsMem_Fsize(const char *path, size_t *fsize)
{
    int32_t result = 0;

    mem_file_idx_t idx = ADKFS_MEM_INVALID_FILE_IDX;
    if (find_file_by_path(path, &idx))
    {
        *fsize = adkfs_mem_file_mgr[idx]->blob.data_length;
    }
    else
    {
        result = ENOENT;
    }

    return result;
}

static int32_t AdkFsMem_Fmap(mem_file_handle_t *handle, const void **fbuffer)
{
    if(ADKFS_FOPEN_READ != handle->access)
    {
        return EACCES;
    }

    int32_t result = 0;

    DEBUG_LOG("AdkFsMem_Fmap: index=%u, offset=%u", handle->idx, handle->offset);
    mem_file_t *file = adkfs_mem_file_mgr[handle->idx];

    if (!file)
    {
        DEBUG_LOG_ERROR("AdkFsMem_Fmap: Failed to map file index=%u, offset=%u", handle->idx, handle->offset);
        result = ENOENT;
    }

    *fbuffer = file->blob.data + handle->offset;
    return result;
}

static int32_t AdkFsMem_Funmap(mem_file_handle_t *handle)
{
    int32_t result = 0;
    mem_file_t *file = adkfs_mem_file_mgr[handle->idx];
    if (!file)
    {
        result = ENOENT;
    }
    return result;
}

static int32_t AdkFsMem_Fseek(mem_file_handle_t *handle, size_t offset)
{
    handle->offset = offset;
    return 0;
}

int32_t AdkFsMem_Ftell(mem_file_handle_t *handle, size_t* offset)
{
    *offset = handle->offset;
    return 0;
}

static size_t truncate_read_to_fsize(size_t offset, size_t read_size, size_t fsize)
{
    if((offset + read_size) > fsize)
    {
        return fsize - offset;
    }

    return read_size;
}

static int32_t AdkFsMem_Fread(mem_file_handle_t *handle, void *buf_ptr, size_t read_size, size_t *bytes_read)
{
    if(!(handle->access & ADKFS_FOPEN_READ))
    {
        return EACCES;
    }

    PanicFalse(read_size > 0);

    DEBUG_LOG("AdkFsMem_Fread: idx=%u, buf_ptr=%p, read_size=%d", handle->idx, buf_ptr, read_size);

    int32_t result = 0;
    mem_file_t *file = adkfs_mem_file_mgr[handle->idx];
    if (file)
    {
        if (((uintptr_t)file->blob.data < (uintptr_t)buf_ptr) && ((uintptr_t)buf_ptr < (uintptr_t)(file->blob.data + file->blob.data_length)))
        {
            /** If buf_ptr is in this range it means that the file contents have been mapped with Fmap
             * But update the value of bytes_read because the caller may be expecting that
             */
            *bytes_read = read_size;
        }
        else
        {
            *bytes_read = truncate_read_to_fsize(handle->offset, read_size, file->blob.data_length);
            memcpy(buf_ptr, (file->blob.data + handle->offset), *bytes_read);
        }
    }
    else
    {
        result = ENOENT;
    }

    return result;
}

static int32_t AdkFsMem_Fwrite(mem_file_handle_t *handle, const void *buf_ptr, size_t write_size, size_t *bytes_written)
{
    if(!(handle->access & (ADKFS_FOPEN_WRITE|ADKFS_FOPEN_APPEND)))
    {
        return EACCES;
    }

    int32_t result = 0;
    mem_file_t *file = adkfs_mem_file_mgr[handle->idx];
    if (file)
    {
        size_t end_of_write = handle->offset + write_size;

        if (!file->blob.data)
        {
            file->blob = DataBlob_Create(end_of_write);
            memset(file->blob.data, 0, end_of_write);
        }
        else if (file->blob.data_length < end_of_write)
        {
            file->blob.data = realloc(file->blob.data, end_of_write);
            memset(file->blob.data + file->blob.data_length, 0, end_of_write - file->blob.data_length);
            file->blob.data_length = end_of_write;
        }

        memcpy(file->blob.data + handle->offset, buf_ptr, write_size);
        handle->offset = end_of_write;

        *bytes_written = write_size;
    }
    else
    {
        result = ENOENT;
    }

    return result;
}

static int32_t AdkFsMem_Fclose(mem_file_handle_t *handle)
{
    int32_t result = 0;
    mem_file_t *file = adkfs_mem_file_mgr[handle->idx];

    if (!file)
    {
        DEBUG_LOG_ERROR("AdkFsMem_Fclose: ENOENT, index=%u, offset=%u", handle->idx);
        return ENOENT;
    }

    for (size_t i = 0; i < ARRAY_DIM(file->handles); i++)
    {
        if (&file->handles[i] == handle)
        {
            reset_handle(&file->handles[i]);
            break;
        }
    }

    return result;
}

static int32_t AdkFsMem_Fdelete(const char *path)
{
    mem_file_idx_t idx = ADKFS_MEM_INVALID_FILE_IDX;
    if (find_file_by_path(path, &idx))
    {
        mem_file_t *file = adkfs_mem_file_mgr[idx];
        DataBlob_Free(&file->blob);

        free(file->path);
        file->path = NULL;

        for (size_t i = 0; i < ARRAY_DIM(file->handles); i++)
        {
            reset_handle(&file->handles[i]);
        }
        free(file);
        adkfs_mem_file_mgr[idx] = NULL;
        return 0;
    }
    return EPERM;
}

static const adkfs_provider_if_t adkfs_mem_if = {
    .root_path = "/mem",
    .Fopen = AdkFsMem_Fopen,
    .Fsize = AdkFsMem_Fsize,
    .Fmap = AdkFsMem_Fmap,
    .Funmap = AdkFsMem_Funmap,
    .Fseek = AdkFsMem_Fseek,
    .Ftell = AdkFsMem_Ftell,
    .Fread = AdkFsMem_Fread,
    .Fwrite = AdkFsMem_Fwrite,
    .Fclose = AdkFsMem_Fclose,
    .Fdelete = AdkFsMem_Fdelete,
};

const adkfs_provider_if_t* AdkFsMem_GetProvider(void)
{
    return &adkfs_mem_if;
}

bool AdkFs_MemInit(Task init_task)
{
    UNUSED(init_task);
    AdkFs_RegisterProvider(AdkFsMem_GetProvider());

    return TRUE;
}

#ifdef HOSTED_TEST_ENVIRONMENT
void AdkFsMem_Shutdown(void)
{
    for (size_t i = 0; i < ADKFS_MEM_MAX_OPEN_FILES; i++)
    {
        mem_file_t *file = adkfs_mem_file_mgr[i];
        if (file)
        {
            free(file);
            adkfs_mem_file_mgr[i] = NULL;
        }
    }
}
#endif /* HOSTED_TEST_ENVIRONMENT */